package receiver;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.zip.Adler32;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import common.Constants;
import common.Header;
import common.PacketType;
import common.Utilities;

/**
 * Main Function to accept file from sender.
 * @author Brad & Adam
 */
public class ReceiveFileMain {
  private static boolean                          TIMESTAMPS   = false;
  private static boolean                          DEBUG        = false;
  private static final int                        BUFFERSIZE   = 1000;
  private static DatagramSocket                   socket;
  private static int                              receiverPort = 0;
  private static int                              startSeqNum  = 0;
  private static long                             startTime    = 0;
  private static int                              ackNum       = 0;
  private static DatagramPacket                   lastWrittenPacket;
  private static HashMap<Integer, DatagramPacket> packetBuffer = 
    new HashMap<Integer, DatagramPacket>();
  private static String                           fileName     = "";
  private static FileOutputStream                 fos          = null;
  private static BufferedOutputStream             bos          = null;
  private static int                              offset       = 0;
  private static boolean                          isReceiving  = false;
  
  /**
   * Main.
   * @param args
   */
  public static void main(String[] args) {
    // Parse command line.
    getArgs(args);
    // Set up socket to listen on that port (DatagramSocket).
    openSocket();
    // Listen for the opening packet, and parse in packets as they come.
    listen();
  }
  
  /**
   * Opens the Datagram Socket to listen on.
   */
  private static void openSocket() {
    try {
      // Open the Socket.
      socket = new DatagramSocket(receiverPort);
    } catch (SocketException e) {
      e.printStackTrace();
    }
  }
  
  /**
   * Opens a File to write to.
   */
  private static void openStream(String fileName) {
    try {
      File theFile = new File(fileName);
      fos = new FileOutputStream(theFile);
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    }
    // The Stream to write to a file.
    bos = new BufferedOutputStream(fos);
  }
  
  /**
   * Waits for a packet, and interprets it. Closes the File when done.
   */
  private static void listen() {
    // Initialization.
    byte[] dataBuffer = new byte[Constants.DATA_SIZE + Header.HEADER_SIZE];
    DatagramPacket packet = new DatagramPacket(dataBuffer, dataBuffer.length);
    try {
      while (true) {
        // Listen for Incoming Packets.
        socket.receive(packet);
        // Get Header, Sequence Number.
        Header header = Utilities.extractHeader(packet);
        int seqNum = header.getSeqNum();
        // Check Checksum.
        if (Utilities.correctChecksum(packet)) {
          // If a new file comes through.
          if (header.getPacketType() == PacketType.PACKET_START && !isReceiving) {
            // Set starting Ack Num.
            ackNum = seqNum;
            // The Next Packet will be the start of the File.
            startSeqNum = seqNum + 1;
            // get the Time when started.
            startTime = System.currentTimeMillis();
            // Parse the Start Packet.
            parse(packet);
            // Status, Accepted Packet.
            status(packet, "1");
          // Check the Ack Number.
          } else if (seqNum == (ackNum + 1)) {
            // Acknowledge up to the newest packet that has been read in.
            ackNum = seqNum;
            // Parse & Print Status.
            parse(packet);
            // Status, Accepted Packet.
            status(packet, "1");
          } else if (seqNum > (ackNum + 1)) {
            if (packetBuffer.containsKey(seqNum)) {
              // Buffer the packet
              packetBuffer.put(seqNum, packet);
              // Status, Buffered
              status(packet, "2");
            } else {
              // Status, Ignored
              status(packet, "3");
            }
          } else if (seqNum < (ackNum + 1)) {
            // Status, Ignored
            status(packet, "3");
          }
        } else {
          // Print Corrupt Status.
          status(packet, "5");
        }
        // Send the Ack.
        sendAck(lastWrittenPacket,
        		Utilities.extractHeader(lastWrittenPacket).getPacketType());
        // Status, Sent an Ack.
        status(packet, "7");
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
  
  private static void parse(DatagramPacket packet) throws IOException {
    // Initialization.
    Header header = Utilities.extractHeader(packet);
    // Get the sequence number.
    int seqNum = header.getSeqNum();
    // If There is a Start Packet.
    if (!isReceiving) {
      if (header.getPacketType() == PacketType.PACKET_START) {
        // Receive Start.
        receiveStart(packet);
        // Set receiving to true.
        isReceiving = true;
        // Start with a blank buffer.
        packetBuffer.clear();
        // Set Packet to Ack.
        lastWrittenPacket = packet;
      } else if (header.getPacketType() == PacketType.PACKET_DATA
                 || header.getPacketType() == PacketType.PACKET_FINISH) {
        packetBuffer.put(seqNum, packet);
      } else {
        // This would mean its corrupt.
      }
    } else if (isReceiving) {
      if (header.getPacketType() == PacketType.PACKET_DATA) {
        recieveData(packet);
        // Set Packet to Ack.
        lastWrittenPacket = packet;
      } else if (header.getPacketType() == PacketType.PACKET_FINISH) {
        // Receive Finish.
        recieveFinish(packet);
        // Set receiving to false.
        isReceiving = false;
        // Set Packet to Ack.
        lastWrittenPacket = packet;
      } else {
        // This would mean it is a duplicate Start or corrupt.
      }
    }
    // Check if the next one is ready.
    if (packetBuffer.containsKey(ackNum + 1)) {
      // Get the next Packet from the queue.
      packet = packetBuffer.get(ackNum + 1);
      packetBuffer.remove(ackNum + 1);
      // Acknowledge up to the newest packet that has been read in.
      ackNum = seqNum;
      // Once the write to file code is abstracted call that.
      parse(packet);
    }
  }
  
  /**
   * Receive Start.
   * @param packet
   * @throws IOException
   */
  private static void receiveStart(DatagramPacket packet) throws IOException {
    byte[] serializedFileName = Utilities.extractData(packet);
    fileName = (Utilities.deserializeString(serializedFileName).trim() + ".recv");
    // Open the file.
    openStream(fileName);
    // Set Packet to Ack.
    lastWrittenPacket = packet;
    // Debug.
    status(packet, "0");
  }
  
  /**
   * Receive Data.
   * @param packet
   * @param header
   * @throws IOException
   */
  private static void recieveData(DatagramPacket packet) throws IOException {
    status(packet, "6");
    // Get Data.
    byte[] data = Utilities.extractData(packet);
    // Set Packet to Ack.
    lastWrittenPacket = packet;
    fos.write(data);
  }
  
  /**
   * Receive Finish.
   * @param packet
   * @param h
   * @throws IOException
   */
  private static void recieveFinish(DatagramPacket packet) throws IOException {
    status(packet, "4");
    // Close the File.
    bos.close();
    fos.close();
    // Set Packet to Ack.
    lastWrittenPacket = packet;
    // Status & Debug.
  }
  
  /**
   * Send Acknowledge Packet.
   * @param packet
   * @param ackNum
   * @throws IOException
   */
  private static void sendAck(DatagramPacket packet, byte type) throws IOException {
    short window = (short) (ReceiveFileMain.BUFFERSIZE - packetBuffer.size());
    // Create the Ack.
    Header ackHeader = new Header(ackNum, window, type);
    long checkSum = Utilities.computeCheckSum(ackHeader.serialize());
    ackHeader.setCheckSum(checkSum);
    DatagramPacket ack = new DatagramPacket(ackHeader.serialize(),
                                            Header.HEADER_SIZE,
                                            packet.getAddress(),
                                            packet.getPort());
    // Send the Ack.
    socket.send(ack);
  }
  
  /**
   * Debug.
   * Based on the number (string) Passed print a different status message.
   * 0 :: Started.
   * 1 :: Accepted in order.
   * 2 :: Accepted out of order.
   * 3 :: Ignored
   * 4 :: Completed
   * 5 :: Corrupt.
   * 6 :: Written
   * 7 :: Acknowledge
   * @param packet
   * @param status
   */
  private static void status(DatagramPacket packet, String status) throws IOException {
    // Data Packet Number.
    int number = (Header.deserialize(packet.getData()).getSeqNum() - startSeqNum);
    // Offset.
    int offset = Math.max(0, (number * Constants.DATA_SIZE));
    // Data Size.
    int dataSize = 0;
    if (Header.deserialize(packet.getData()).getPacketType() == PacketType.PACKET_DATA) {
      dataSize = packet.getLength() - Header.HEADER_SIZE;
    }
    // Message.
    String msg = "[recvdata] " + offset + " (" + dataSize + ") ";
    // Status
    if (status == "0") {
      msg = "[started] " + fileName;
    } else if (status == "1") {
      msg += "ACCEPTED(in-order)";
    } else if (status == "2") {
      msg += "ACCEPTED(out-of-order)";
    } else if (status == "3") {
      msg += "IGNORED";
    } else if (status == "4") {
      msg = "[completed] in " + (System.currentTimeMillis() - startTime) + " ms";
    } else if (status == "5") {
      msg = "[recv corrupt packet]";
    } else if (status == "6") {
      msg = "[writdata] " + offset + " (" + dataSize + ") ";
    } else if (status == "7") {
      msg = "[ACK sent] " + offset + " (" + dataSize + ") ";
    }
    // Time Stamps.
    if (TIMESTAMPS == true) {
      msg = System.currentTimeMillis() + " :: " + msg;
    }
    // Print
    System.out.println(msg);
  }
  
  /**
   * Is essentially GetOpt.
   * @param args
   */
  private static void getArgs(String[] args) {
    // If there are too many arguments, print usage.
    if (args.length < 2) {
      Utilities.printUsage(false);
      System.exit(1);
    }
    for (int i = 0; i < args.length; i++) {
      if (args[i].equals("-p")) {
        receiverPort = new Integer(args[i + 1]);
      }
      if (args[i].equals("-d")) {
        DEBUG = true;
      }
      if (args[i].equals("-t")) {
        TIMESTAMPS = true;
      }
    }
    // Validate inputs.
    if (receiverPort == 0) {
      Utilities.printUsage(false);
      System.exit(1);
    }
  }
}